class Vue {
  constructor(vueOptions) {
    this.$data = vueOptions.data

    // 劫持data中的所有属性，设置set和get方法
    Observer(this.$data)

    // 将data中的数据替换到html节点中的指定地方，并渲染到页面
    Compile(vueOptions.el, this)
  }
}

// 劫持data对象中所有的属性（是所有层次的对象属性哦）
function Observer(data) {

  // 递归出口 如果data没有值 或者 data不再是一个对象类型 则停止递归
  if (!data || typeof data !== 'object') return
  // 创建订阅者收集器
  const dependency = new Dependency()

  // 循环遍历data中或者data中的子对象的所有key
  Object.keys(data).forEach((key) => {
    let val = data[key]
    // 递归调用,劫持所有的属性
    Observer(val)

    // 劫持属性
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      set(newVal) {
        console.log(`属性${key}的值${val}修改为=>${newVal}`)
        val = newVal
        // 如果添加了新的属性，则再次触发劫持
        Observer(newVal)
        // data中的属性值有变化，就需要通知订阅者更新视图
        dependency.notify()
      },
      get() {
        console.log(`访问了属性：${key}=>值：${val}`)
        // 如果有temp（也就是订阅者实例），就将订阅者添加到订阅者数组
        Dependency.temp && dependency.addSub(Dependency.temp)
        if (Dependency.temp) {
          console.log(Dependency.temp)
        }
        return val
      }
    })
  })
}

// 将插值表达式替换为 data对象中的具体数据
function Compile(element, vm) {
  // 获取根节点（#App）对象
  vm.$el = document.querySelector(element)

  //console.log(vm.$el);

  // 创建一个新的空白的文档片段,用来存放解析的页面 ( DocumentFragment)。
  let fragment = document.createDocumentFragment()

  let child // 页面中的某个子节点

  // 通过循环取出页面（其实就是#App下的所有html节点）中的节点，并按照嵌套层级保存到fragment对象中。 
  while (child = vm.$el.firstChild) {
    fragment.append(child) //注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
  }
  //console.log(fragment);
  //console.log(fragment.childNodes);

  // 调用专门替换插值表达式的方法
  fragment_compile(fragment)

  // 将符合插值表达式{{}}格式的文本替换为data对象中相对应的数据（也就是将data中的值赋值到对应的页面）
  function fragment_compile(node) {

    // 匹配{{}}格式数据的正则表达式
    let pattern = /\{\{\s*(\S+)\s*\}\}/

    // 将文本节点类型为3的节点中，{{}}格式的数据替换为data中对应的值
    if (node.nodeType === 3) {
      // 在替换掉插值表达式（{{}}）之前，要将节点的值先保存一份,也就是原始的格式：{{more.like}}  如果不保存，在订阅者更新的时候，pattern正则将无法匹配
      let xxx = node.nodeValue
      // 获取符合正则结果的数据 数据格式： ['{{name}}', 'name', index: 4, input: '学习人：{{name}}', groups: undefined]
      const result_regex = pattern.exec(node.nodeValue)
      //console.log(result_regex);
      // 不符合正则的将会返回null,所以这里需要判断跳过对null的处理
      if (result_regex) {
        let value = result_regex[1].split('.').reduce((pre, curr) => pre[curr], vm.$data)
        //console.log('获取到{{}}模板中设置的值：', value)
        // 将{{}}模板 替换为 真实需要渲染的值（也就是将data对象中的值替换掉 {{}}模板字符）
        node.nodeValue = xxx.replace(pattern, value)
        //console.log('nodeValue中的值替换后为：', node.nodeValue)

        // 在替换模板完成后创建Watcher，这样在数据变动时，这个订阅者就会自己更新自己的视图了
        new Watcher(vm, result_regex[1], newValue => {
          // 将新的值替换到旧的模板中去
          node.nodeValue = xxx.replace(pattern, newValue)
        })
      }
      // 跳出递归调用
      return
    }

    // 如果是input节点，且节点名程为input
    if (node.nodeType === 1 && node.nodeName === 'INPUT') {
      // 将input节点上的 属性(type,v-model)转换成数组
      const arr = Array.from(node.attributes)
      console.log(arr)
      // 循环遍历input节点上的属性
      arr.forEach(i => {
        // 获取属性名称
        if (i.nodeName === 'v-model') {
          // 获取v-model指令绑定在data对象中的值
          const value = i.nodeValue.split('.').reduce((pre, curr) => pre[curr], vm.$data)
          // 将获取到的值，替换到使用v-model指令的节点内容中
          node.value = value
          // 为属性生成一个订阅者
          new Watcher(vm, i.nodeValue, newValue => {
            node.value = newValue
          })
          // 实现双向绑定,用户在input中输入数据时，data中的值跟着变化
          node.addEventListener('input', (e) => {
            let arr1 = i.nodeValue.split('.')
            let arr2 = arr1.slice(0, arr1.length - 1)
            let final = arr2.reduce((pre, curr) => pre[curr], vm.$data)
            final[arr1[arr1.length - 1]] = e.target.value
          })
        }
      })
    }

    // 循环遍历根节点（#App）下的所有节点（换行符也是节点哦，它的节点类型为文本节点）
    node.childNodes.forEach(child => fragment_compile(child))
  }
  // 将替换完成的模板渲染到#App节点下
  vm.$el.appendChild(fragment)
}

// 完成数据劫持和模板的字符替换后 我们接下来需要实现：发布者-订阅者模式
// 创建收集和通知订阅者 的依赖对象 Dependency
class Dependency {
  constructor() {
    // 创建一个数组用来保存所有的订阅者
    this.subscribes = []
  }
  // 收集订阅者
  addSub(sub) {
    this.subscribes.push(sub)
  }
  // 通知订阅者更新视图
  notify() {
    this.subscribes.forEach(sub => sub.update())
  }
}

// 订阅者
class Watcher {
  // vm为vue实例，key为实例中data对象上的属性，每个属性需要一个Watcher进行监听，callback为触发更新时需要调用的回调函数
  constructor(vm, key, callback) {
    this.vm = vm
    this.key = key
    this.callback = callback
    // 临时属性 - 触发get
    Dependency.temp = this
    // 在这里通过获取一次data对象中属性的值，触发属性劫持的get函数
    key.split('.').reduce((pre, curr) => pre[curr], vm.$data)
    Dependency.temp = null
  }

  // 值有变化，就更新视图
  update() {
    let val = this.key.split('.').reduce((pre, curr) => pre[curr], this.vm.$data)
    this.callback(val)
  }

}